﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SnakeGame
{
    public static class Engine
    {
        private const int DefaultWindowWidth = 50;
        private const int DefaultWindowHeight = 30;
        private const int NumberOfScoresToDisplay = 5;
        private static int gameSpeed = 100;

        private const string highScoresFile = @"../../highscores.txt";
        private static Random foodGenerator = new Random();
        private static PlayerScore[] playerScores;
        private static Wall[] allWalls;
        private static Block centerBlock;

        private static int soundFrequency = 1500;
        private static ConsoleColor snakeColor = ConsoleColor.Green;
        private static ConsoleColor foodColor = ConsoleColor.Red;

        public static void PrintInstructions()
        {
            string[] Instructions = 
            {
            "Game Rules:", 
            "1. Use the four arrowkeys to move.",
            "2. Eat the pieces of food to gain length.",
            "3. If you crash into a wall, you die.",
            "4. If you bite your own tail, you die.",
            "5. If you crash into a boundary, you die.",
            "6. Press backspace to pause and unpause.",
            "",
            "Press backspace to return to the main menu."
            };
            int longestLine = Instructions[Instructions.Length - 1].Length;
            Console.Clear();
            for (int i = 0; i < Instructions.Length; i++)
            {
                Console.SetCursorPosition(Console.WindowWidth / 2 - longestLine / 2, 5 + i);
                Console.WriteLine(Instructions[i]);
            }
            GamePause();
            Console.Clear();
            ShowGameMenu();
        }

        public static void PrintAbout()
        {
            Console.Clear();
            string[] Credits = 
            {
            "Prepared by the Rachel Grey Telerik Academy team:", 
            "Deyan Yanakiev, lostm1nd",
            "Christian Rizov, kbrizov",
            "Ivan Atanasov, ivanszbg",
            "Vasil Vasilev, vasil.k.vasilev",
            "Georgi Radev, gradev",
            "",
            "Press backspace to return to the main menu."
            };
            int longestLine = Credits[0].Length;
            Console.Clear();
            for (int i = 0; i < Credits.Length; i++)
            {
                Console.SetCursorPosition(Console.WindowWidth / 2 - longestLine / 2, 5 + i);
                Console.WriteLine(Credits[i]);
            }

            GamePause();
            Console.Clear();
            ShowGameMenu();
        }

        public static void PrintSettings()
        {
            string[] options = 
            {
            "1. Change snake color",
            "2. Change food color",
            "3. Change frequency of eating food sound",
            "",
            "Press backspace to return to the main menu."
            };

            int longestLine = options[options.Length - 1].Length;
            Console.Clear();
            for (int i = 0; i < options.Length; i++)
            {
                Console.SetCursorPosition(Console.WindowWidth / 2 - longestLine / 2, 5 + i);
                Console.WriteLine(options[i]);
            }

            ConsoleKeyInfo userChoice = Console.ReadKey(true);
            while ((userChoice.Key != ConsoleKey.D1 && userChoice.Key != ConsoleKey.NumPad1) &&
                   (userChoice.Key != ConsoleKey.D2 && userChoice.Key != ConsoleKey.NumPad2) &&
                   (userChoice.Key != ConsoleKey.D3 && userChoice.Key != ConsoleKey.NumPad3) &&
                   (userChoice.Key != ConsoleKey.Backspace))
            {
                userChoice = Console.ReadKey(true);
            }

            if (userChoice.Key == ConsoleKey.D1 || userChoice.Key == ConsoleKey.NumPad1)
            {
                ShowSnakeColorSettings();
            }
            else if (userChoice.Key == ConsoleKey.D2 || userChoice.Key == ConsoleKey.NumPad2)
            {
                ShowFoodColorSettings();
            }
            else if (userChoice.Key == ConsoleKey.D3 || userChoice.Key == ConsoleKey.NumPad3)
            {
                ShowSoundFrequencySettings();
            }
            else if (userChoice.Key == ConsoleKey.Backspace)
            {
                Console.Clear();
                ShowGameMenu();
            }
        }

        public static void PrintHighScores()
        {
            if (playerScores == null)
            {
                playerScores = LoadScoresInMemory();
            }

            Console.Clear();

            for (int i = 0; i < NumberOfScoresToDisplay; i++)
            {
                Console.SetCursorPosition(0, 5 + i);
                Console.WriteLine("{0}. {1}", i + 1, playerScores[i]);
            }

            Console.WriteLine("\nPress backspace to return to the main menu.");
            GamePause();
            Console.Clear();
            ShowGameMenu();
        }

        public static void PrintHighScores(PlayerScore currentScore)
        {
            if (playerScores == null)
            {
                playerScores = LoadScoresInMemory();
            }

            Console.Clear();

            for (int i = 0; i < NumberOfScoresToDisplay; i++)
            {
                if (currentScore.Equals(playerScores[i]))
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                }

                Console.WriteLine("{0}. {1}", i + 1, playerScores[i]);
                Console.ResetColor();
            }

            Console.WriteLine("\nPress backspace to return to the main menu.");
            GamePause();
            Console.Clear();
            ShowGameMenu();

        }

        public static void SetPlayfieldSize(
            int windowWidth = DefaultWindowWidth, int windowHeight = DefaultWindowHeight)
        {
            Console.Title = "Snake by Rachel Grey Team";
            Console.WindowWidth = windowWidth;
            Console.WindowHeight = windowHeight;
            Console.BufferHeight = Console.WindowHeight;
            Console.BufferWidth = Console.WindowWidth;

        }

        public static void DrawFood(MatrixCoordinate food)
        {
            if (food == null)
            {
                throw new ArgumentNullException("The instance of the food cannot be null.");
            }

            Console.ForegroundColor = foodColor;
            Console.SetCursorPosition(food.X, food.Y);
            Console.Write('\u00A4');
            Console.ResetColor();
        }

        public static void DrawSnake(Snake snake)
        {
            if (snake == null)
            {
                throw new ArgumentNullException("The instance of the snake cannot be null.");
            }

            foreach (var snakeElement in snake.Body)
            {
                Console.SetCursorPosition(snakeElement.X, snakeElement.Y);
                Console.ForegroundColor = snakeColor;

                if (snakeElement == snake.Head)
                {
                    if (snake.CurrentDirection == Direction.Right)
                    {
                        Console.Write(">");
                    }
                    else if (snake.CurrentDirection == Direction.Down)
                    {
                        Console.Write("v");
                    }
                    else if (snake.CurrentDirection == Direction.Left)
                    {
                        Console.Write("<");
                    }
                    else if (snake.CurrentDirection == Direction.Up)
                    {
                        Console.Write("^");
                    }
                }
                else
                {
                    Console.Write("*");
                }

                Console.ResetColor();
            }
        }

        public static void DrawWall(Wall wall)
        {
            foreach (var wallElement in wall.Body)
            {
                Console.SetCursorPosition(wallElement.X, wallElement.Y);
                Console.Write("#");
            }
        }

        public static void DrawBlock(Block block)
        {
            for (int row = 0; row < block.Rows; row++)
            {
                for (int column = 0; column < block.Columns; column++)
                {
                    Console.SetCursorPosition(block.Body[row, column].X, block.Body[row, column].Y);
                    Console.Write("#");
                }
            }
        }

        public static void ShowGameMenu()
        {
            string[] menuLogo = 
            {
                @"  _____                   _",
                @" / ____|                 | |",
                @"| (___    _ __     __ _  | | __   ___",
                @" \___ \  | '_ \   / _` | | |/ /  / _ \", //longest line; index = 3
                @" ____) | | | | | | (_| | |   <  |  __/",
                @"|_____/  |_| |_|  \__,_| |_|\_\  \___|"
            };
            int logoLongestLine = menuLogo[3].Length;

            string[] menuText =
            {
                "1. Start Game",
                "2. Settings",
                "3. High Scores",
                "4. Instructions", //longest line; index = 2
                "5. About",
                "6. Exit"
            };
            int textLongestLine = menuText[2].Length;

            int startHeight = 5;
            Console.ForegroundColor = ConsoleColor.Green;
            for (int i = 0; i < menuLogo.Length; i++)
            {
                Console.SetCursorPosition(Console.WindowWidth / 2 - logoLongestLine / 2, startHeight);
                Console.WriteLine(menuLogo[i]);
                startHeight++;
            }
            Console.ResetColor();

            startHeight += 2; //two empty line between logo and text
            for (int i = 0; i < menuText.Length; i++)
            {
                Console.SetCursorPosition(Console.WindowWidth / 2 - textLongestLine / 2,
                    startHeight);
                Console.WriteLine(menuText[i]);
                startHeight++;
            }

            ConsoleKeyInfo userChoice = Console.ReadKey(true);
            while ((userChoice.Key != ConsoleKey.D1 && userChoice.Key != ConsoleKey.NumPad1) &&
                   (userChoice.Key != ConsoleKey.D2 && userChoice.Key != ConsoleKey.NumPad2) &&
                   (userChoice.Key != ConsoleKey.D3 && userChoice.Key != ConsoleKey.NumPad3) &&
                   (userChoice.Key != ConsoleKey.D4 && userChoice.Key != ConsoleKey.NumPad4) &&
                   (userChoice.Key != ConsoleKey.D5 && userChoice.Key != ConsoleKey.NumPad5) &&
                   (userChoice.Key != ConsoleKey.D6 && userChoice.Key != ConsoleKey.NumPad6))
            {
                userChoice = Console.ReadKey(true);
            }

            if (userChoice.Key == ConsoleKey.D1 || userChoice.Key == ConsoleKey.NumPad1)
            {
                StartGame();
            }
            else if (userChoice.Key == ConsoleKey.D2 || userChoice.Key == ConsoleKey.NumPad2)
            {
                PrintSettings();
            }
            else if (userChoice.Key == ConsoleKey.D3 || userChoice.Key == ConsoleKey.NumPad3)
            {
                PrintHighScores();
            }
            else if (userChoice.Key == ConsoleKey.D4 || userChoice.Key == ConsoleKey.NumPad4)
            {
                PrintInstructions();
            }
            else if (userChoice.Key == ConsoleKey.D5 || userChoice.Key == ConsoleKey.NumPad5)
            {
                PrintAbout();
            }
            else if (userChoice.Key == ConsoleKey.D6 || userChoice.Key == ConsoleKey.NumPad6)
            {
                Environment.Exit(0);
            }
        }

        private static void ShowSnakeColorSettings()
        {
            snakeColor = SelectColor();
            Console.Clear();
            ShowGameMenu();
        }

        private static void ShowFoodColorSettings()
        {
            foodColor = SelectColor();
            Console.Clear();
            ShowGameMenu();
        }

        private static void ShowSoundFrequencySettings()
        {
            Console.Clear();

            string message = "Input a frequency ranging from 37 to 32767 hertz: ";
            Console.SetCursorPosition(Console.WindowWidth / 2 - message.Length / 2, 5);
            Console.Write(message);

            Console.SetCursorPosition(Console.WindowWidth / 2 - message.Length / 2, 6);
            soundFrequency = int.Parse(Console.ReadLine());

            Console.WriteLine("\nPress backspace to return to the main menu.");
            GamePause();
            Console.Clear();
            ShowGameMenu();
        }

        private static ConsoleColor SelectColor()
        {
            string[] options = 
            {
            "1.  Blue",
            "2.  Dark Blue",
            "3.  Cyan",
            "4.  Dark Cyan",
            "5.  Green",
            "6.  Dark Green",
            "7.  Magenta",
            "8.  Dark Magenta",
            "9.  Red",
            "10. Dark Red",
            "11. Yellow",
            "12. Dark Yellow",
            "13. White"
            };

            int longestLine = options[options.Length - 1].Length;
            Console.Clear();
            for (int i = 0; i < options.Length; i++)
            {
                Console.SetCursorPosition(Console.WindowWidth / 2 - longestLine / 2, 5 + i);
                Console.WriteLine(options[i]);
            }

            int colorNumber = int.Parse(Console.ReadLine());
            ConsoleColor color;

            switch (colorNumber)
            {
                case 1:
                    color = ConsoleColor.Blue;
                    break;

                case 2:
                    color = ConsoleColor.DarkBlue;
                    break;

                case 3:
                    color = ConsoleColor.Cyan;
                    break;

                case 4:
                    color = ConsoleColor.DarkCyan;
                    break;

                case 5:
                    color = ConsoleColor.Green;
                    break;

                case 6:
                    color = ConsoleColor.DarkGreen;
                    break;

                case 7:
                    color = ConsoleColor.Magenta;
                    break;

                case 8:
                    color = ConsoleColor.DarkMagenta;
                    break;

                case 9:
                    color = ConsoleColor.Red;
                    break;

                case 10:
                    color = ConsoleColor.DarkRed;
                    break;

                case 11:
                    color = ConsoleColor.Yellow;
                    break;

                case 12:
                    color = ConsoleColor.DarkYellow;
                    break;

                case 13:
                    color = ConsoleColor.White;
                    break;

                default:
                    color = ConsoleColor.Green;
                    break;
            }

            return color;
        }

        public static void GenerateWalls()
        {
            int blockSideLength = 3;
            int blockCoordinateX = DefaultWindowWidth / 2 - blockSideLength / 2;
            int blockCoordinateY = DefaultWindowHeight / 2 - blockSideLength / 2;
            centerBlock = new Block(blockCoordinateX, blockCoordinateY, blockSideLength, blockSideLength);

            int wallLength = 3;
            int positionAdjustment = 4;
            int upperVerticalCoordinateX = 1 + positionAdjustment;
            int upperVerticalCoordinateY = 1 + positionAdjustment;
            Wall upperVertical = new Wall(upperVerticalCoordinateX, upperVerticalCoordinateY, wallLength, false);

            int upperHorizontalCoordinateX = 1 + positionAdjustment;
            int upperHorizontalCoordinateY = 1 + positionAdjustment;
            Wall upperHorizontal = new Wall(upperHorizontalCoordinateX, upperHorizontalCoordinateY, wallLength, true);

            int lowerVerticalCoordinateX = DefaultWindowWidth - 2 - positionAdjustment;
            int lowerVerticalCoordinateY = DefaultWindowHeight - wallLength - 1 - positionAdjustment;
            Wall lowerVertical = new Wall(lowerVerticalCoordinateX, lowerVerticalCoordinateY, wallLength, false);

            int lowerHorizontalCoordinateX = DefaultWindowWidth - wallLength - 1 - positionAdjustment;
            int lowerHorizontalCoordinateY = DefaultWindowHeight - 2 - positionAdjustment;
            Wall lowerHorizontal = new Wall(lowerHorizontalCoordinateX, lowerHorizontalCoordinateY, wallLength, true);

            allWalls = new Wall[]
            {
                upperVertical,
                upperHorizontal,
                lowerVertical,
                lowerHorizontal
            };
        }

        public static void StartGame()
        {
            int currentScore = 0;
            playerScores = LoadScoresInMemory();

            int snakeStartRow = Console.WindowHeight / 2;
            Snake snake = new Snake(5, snakeStartRow, Direction.Right);

            GenerateWalls();

            MatrixCoordinate food = new MatrixCoordinate(foodGenerator.Next(Console.WindowWidth),
                foodGenerator.Next(Console.WindowHeight));

            while (true)
            {
                bool gameIsOver = false;

                HandleButtonInput(snake);
                HandleCollisions(snake, food, ref currentScore, ref gameIsOver);

                if (gameIsOver)
                {
                    return;
                }
                else
                {
                    Console.Clear();
                    snake.Move();
                    DrawBlock(centerBlock);
                    foreach (var wall in allWalls)
                    {
                        DrawWall(wall);
                    }
                    DrawFood(food);
                    DrawSnake(snake);

                    Thread.Sleep(gameSpeed);
                }
            }
        }

        public static void GamePause()
        {
            ConsoleKeyInfo userChoice = Console.ReadKey(true);
            while (userChoice.Key != ConsoleKey.Backspace)
            {
                userChoice = Console.ReadKey(true);
            }

            Thread.Sleep(gameSpeed);
        }

        private static bool IsSnakeDead(Snake currentSnake)
        {
            SnakeBodyElement nextHead = currentSnake.GetNextHead();

            if (nextHead.X < 0 || nextHead.X >= Console.WindowWidth ||
                nextHead.Y < 0 || nextHead.Y >= Console.WindowHeight ||
                currentSnake.Body.Contains(nextHead))
            {
                return true;
            }

            if (IsWithinWallsOrBlock(nextHead))
            {
                return true;
            }

            return false;
        }

        private static bool SnakeAteFood(Snake currentSnake, MatrixCoordinate food)
        {
            if (currentSnake == null)
            {
                throw new ArgumentNullException("The instance of the snake cannot be null.");
            }

            if (food == null)
            {
                throw new ArgumentNullException("The instance of the food cannot be null.");
            }

            SnakeBodyElement nextHead = currentSnake.GetNextHead();
            if (nextHead == food)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        private static bool IsWithinWallsOrBlock(MatrixCoordinate toCheck)
        {
            for (int i = 0; i < allWalls.Length; i++)
            {
                if (allWalls[i].Body.Contains(toCheck))
                {
                    return true;
                }
            }

            for (int row = 0; row < centerBlock.Rows; row++)
            {
                for (int column = 0; column < centerBlock.Columns; column++)
                {
                    if (centerBlock.Body[row, column] == toCheck)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        private static void HandleCollisions(Snake snake, MatrixCoordinate food, ref int currentScore, ref bool gameIsOver)
        {
            if (SnakeAteFood(snake, food))
            {
                snake.Grow();
                currentScore += 10;

                food.X = foodGenerator.Next(Console.WindowWidth);
                food.Y = foodGenerator.Next(Console.WindowHeight);
                Console.Beep(soundFrequency, 30);
            }

            while (snake.Body.Contains(food) || IsWithinWallsOrBlock(food))
            {
                food.X = foodGenerator.Next(Console.WindowWidth);
                food.Y = foodGenerator.Next(Console.WindowHeight);
            }

            if (IsSnakeDead(snake))
            {
                int maxPlayerScore = playerScores[0].Score;
                string message = null;
                Console.Beep(400, 100);

                if (currentScore > maxPlayerScore)
                {
                    maxPlayerScore = currentScore;
                    message = string.Format("Game over! New high score: {0} ", maxPlayerScore);
                }
                else
                {
                    message = string.Format("Game over! Your score: {0} ", currentScore);
                }

                Console.Clear();
                Console.ForegroundColor = ConsoleColor.Red;

                Console.SetCursorPosition(
                    (Console.WindowWidth / 2 - message.Length / 2), (Console.WindowHeight / 2));

                Console.WriteLine(message);

                Console.SetCursorPosition(
                    (Console.WindowWidth / 2 - message.Length / 2), (Console.WindowHeight / 2) + 1);

                Console.Write("Enter your name: ");
                string name = Console.ReadLine();

                int length = playerScores.Length;
                PlayerScore currentPlayerScore = new PlayerScore(name, currentScore);

                if (playerScores[length - 1] < currentPlayerScore)
                {
                    playerScores[length - 1] = currentPlayerScore;
                    Array.Sort(playerScores);
                    playerScores = playerScores.Reverse().ToArray();
                    WriteScoresToFile(playerScores);
                }

                Console.ResetColor();
                gameIsOver = true;
                PrintHighScores(currentPlayerScore);
            }
        }

        private static void HandleButtonInput(Snake snake)
        {
            if (snake == null)
            {
                throw new ArgumentNullException("The instance of the snake cannot be null.");
            }

            if (Console.KeyAvailable)
            {
                ConsoleKeyInfo userButtonInput = Console.ReadKey();

                // Clearing the button buffer for smoother experience.
                while (Console.KeyAvailable)
                {
                    Console.ReadKey(true);
                }

                if (userButtonInput.Key == ConsoleKey.RightArrow)
                {
                    snake.TurnRight();
                }

                if (userButtonInput.Key == ConsoleKey.LeftArrow)
                {
                    snake.TurnLeft();
                }

                if (userButtonInput.Key == ConsoleKey.UpArrow)
                {
                    snake.TurnUp();
                }

                if (userButtonInput.Key == ConsoleKey.DownArrow)
                {
                    snake.TurnDown();
                }

                if (userButtonInput.Key == ConsoleKey.Backspace)
                {
                    GamePause();
                }
            }
        }

        private static PlayerScore[] LoadScoresInMemory()
        {
            try
            {
                PlayerScore[] scores = new PlayerScore[NumberOfScoresToDisplay];
                StreamReader reader = new StreamReader(highScoresFile);
                using (reader)
                {
                    string line = null;
                    for (int i = 0; i < NumberOfScoresToDisplay; i++)
                    {
                        line = reader.ReadLine();
                        if (line != null && line != string.Empty)
                        {
                            scores[i] = PlayerScore.Parse(line);
                        }
                        else
                        {
                            scores[i] = new PlayerScore("Undefined", 0, DateTime.MinValue);
                        }
                    }
                }

                return scores;
            }
            catch (FileNotFoundException ex)
            {
                throw new FileNotFoundException(
                    "Loading scores failed. Highscores file missing or corrupt.", ex);
            }
        }

        private static void WriteScoresToFile(PlayerScore[] scores)
        {
            StreamWriter writer = new StreamWriter(highScoresFile, false);
            using (writer)
            {
                foreach (var scoreEntry in scores)
                {
                    writer.WriteLine(scoreEntry.ToString());
                }
            }
        }
    }
}
